Skip to content

fix(oauth): send a bare Bearer challenge when a request has no credentials - #230

Open
Jordan Harband (ljharb) wants to merge 2 commits into
mainfrom
fix/oauth-bare-challenge-on-connect
Open

Jordan Harband (ljharb) wants to merge 2 commits into
mainfrom
fix/oauth-bare-challenge-on-connect

Conversation

@ljharb

@ljharb Jordan Harband (ljharb) commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

When OAuth is enabled, a request with no Authorization header, or with a scheme other than Bearer, now gets the RFC 6750 §3.1 challenge with no error code. This is the form the MCP authorization spec shows:

WWW-Authenticate: Bearer resource_metadata="https://mcp.socket.dev/.well-known/oauth-protected-resource", scope="packages:list alerts:list threat-feed:list"

Before, it also carried error="invalid_request", error_description="…". Unchanged:

  • A Bearer header with no token, and every token-validation failure, keep their error codes.
  • The JSON body still names the problem (RFC 6750 doesn't define the body, and the MCP SDK client reads only the header).
  • Non-OAuth HTTP (API key per request) and stdio are untouched.

scope= is SOCKET_OAUTH_REQUIRED_SCOPES minus offline_access, the same list as PRM scopes_supported. Clients therefore ask for the resource's read scopes, not the authorization server's full list.

Why: Cursor hosted OAuth

Cursor only enters its needsAuth / Authenticate state when it gets a 401 during the connect phase (forum thread).

On main, routeRequest already authenticates every MCP request, initialize and tools/list included. But a probe of production on 2026-11-04 shows behavior that isn't in this repo:

Request to https://mcp.socket.dev/ Live response
anonymous initialize 200 (SSE)
anonymous tools/call 401 error="invalid_request", error_description="Authentication required for this tool", resource_metadata="…" (no scope=)
initialize with a bogus Bearer 401 invalid_token …, scope="packages:list alerts:list threat-feed:list" (matches main)

The string Authentication required for this tool doesn't appear in this repo or its history. The hosted deployment needs a change too. Merging this alone won't move the Cursor row in the README compatibility matrix. Once anonymous initialize on production returns 401, re-test that row, and the Claude Code, Codex and Zed rows (their pre-login flow changes).

Tests

  • Integration (http-replicas.test.mts): exact challenge on anonymous initialize and tools/list through real replicas, with the hosted scope set and a matching scopes_supported.
  • Unit: bare challenge for a missing header and for a non-Bearer scheme; invalid_request kept for Bearer / bearer with no token; lowercase bearer <token> still reaches introspection (mutation-checked); offline_access dropped from scope=; scope= omitted when no scopes are configured; writeOAuthChallenge header and body.

I ran the tests locally without the fleet pack, which couldn't be fetched, using a minimal vitest config. 525/525 pass. The suites that import scripts/fleet/** didn't load. CI is the real gate for lint and those suites.

🤖 Generated with Claude Code


Note

Medium Risk
Changes OAuth 401 response shape on the MCP auth path, which can affect client sign-in behavior, but scope is limited to pre-credential cases and aligns with RFC/MCP expectations.

Overview
OAuth-enabled MCP requests with no Authorization header or a non-Bearer scheme now get a bare WWW-Authenticate challenge (Bearer resource_metadata=…, scope=…) without error= / error_description= in the header, per RFC 6750 §3.1 and the MCP connect-handshake signal clients like Cursor expect on initialize / tools/list.

Adds buildBearerChallenge and writeOAuthChallenge in http.mts, refactors writeOAuthError to share header assembly, and routes missing / wrong-scheme cases in authenticateRequest through the new helper. Malformed Bearer (scheme present but no token) and post-introspection failures still use coded writeOAuthError challenges; the JSON body still describes the problem.

Tests assert the exact challenge on anonymous initialize and tools/list, Bearer case handling, offline_access omitted from scope=, and unit coverage for the new writers.

Reviewed by Cursor Bugbot for commit 211cf16. Configure here.

@ljharb

Copy link
Copy Markdown
Contributor Author

(before I land this, I'm going to put up another PR removing the failing GHCR stuff)

…tials

An OAuth-enabled server already challenges every MCP request, initialize
and tools/list included. A request with no Authorization header, or with
a scheme other than Bearer, now gets the RFC 6750 §3.1 form the MCP
authorization spec shows:

  WWW-Authenticate: Bearer resource_metadata="…", scope="…"

with no error code, instead of error="invalid_request". The scope list
is the resource's own scopes (offline_access dropped), so clients do not
fall back to the issuer's full scopes_supported. The JSON body still
names the problem. A "Bearer" header with no token, and every
token-validation failure, keep their error codes. Non-OAuth HTTP and
stdio are unchanged.

Tests pin the exact challenge on anonymous initialize and tools/list
across replicas, with the hosted scope set, plus the no-scope and
offline_access cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jdalton

Copy link
Copy Markdown
Collaborator

Landed some fixes, Jordan Harband (@ljharb) try again with an updated branch.

@ljharb
Jordan Harband (ljharb) force-pushed the fix/oauth-bare-challenge-on-connect branch from 355c5e5 to 3914ac6 Compare November 5, 2026 21:43

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants